package util.sharedmemory;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.Date;

/**
 * A shared memory segment (smseg) wrapper.
 * Uses the Linux shared-memory implementation.
 * 
 * @author adi
 */
public class SharedMemory {


	/* The location of the shared memory segment */
	private String location;

	/* The size in bytes of the shared memory segment */
	private int size;
	
	/* The id of the shared memory segment */
	private int smsegId;
	
	/* The address of the beginning of the mmaped object */
	private long mmapAddress;
	
	/* The address of this shared memory segment rw-lock (shared between processes) */
	private long rwlockAddress;
	
	/* Keeps the current number of times the rw-lock was opened for read and write
	 * (to allow an upgrade of a read-lock to a write-lock, and nested-locking) */
	private int rwlockReadsCounter;
	private int rwlockWritesCounter;
	private int rwlockOpenType; /* 0 for read, 1 for write */
	
	private final static String SHAREDMEM_LIB = "nativesharedmem";

	static {
		try {
			System.loadLibrary(SHAREDMEM_LIB);
		} catch (UnsatisfiedLinkError e) {
			//System.load("/media/old_drive/home/adi/studies/DSP122/project/workspace/SharedMemoryLib/dist/lib" + SHAREDMEM_LIB + ".so");
			loadFromJar();
		}
		//System.loadLibrary("rt");
	}

	/**
	 * When packaged into JAR extracts DLLs, places these into
	 */
	private static void loadFromJar() {
		// we need to put both DLLs to temp dir
		String path = "SM_" + new Date().getTime();
		loadLib(path, SHAREDMEM_LIB);
	}

	/**
	 * Puts library to temp dir and loads to memory
	 */
	private static void loadLib(String path, String name) {
		name = "/lib" + name + ".so";

		// Lock the tmp directory (to avoid a race-condition between two java processes that execute this concurrently)
		
		//FileLock tmpLock = null;
		//FileChannel tmpChannel = null;
		
		try {
			String tmpDirName = System.getProperty("java.io.tmpdir");
			//tmpChannel = new FileInputStream(tmpDirName).getChannel();
			//tmpLock = tmpChannel.lock();
			
			// have to use a stream
			InputStream in = SharedMemory.class.getResourceAsStream(name);
			// always write to different location
			File tmpDir = new File(tmpDirName + "/" + path);
			if (!tmpDir.exists()) {
				tmpDir.mkdir();
			}
			File fileOut = new File(tmpDirName + "/" + path + name);
			// Create the output file
			OutputStream out = new FileOutputStream(fileOut);
			// Copy the input file to the output file
			int b;
			while ((b = in.read()) != -1) {
				out.write(b);
			}
			in.close();
			out.close();
			System.load(fileOut.toString());
		} catch (Exception e) {
			throw new RuntimeException("Failed to load required library", e);
		} finally {
			/*
			// unlock
			if (tmpLock != null) {
				try {
					tmpLock.release();
				} catch (IOException e) {
				}
			}
			// release channel
			if (tmpChannel != null) {
				try {
					tmpChannel.close();
				} catch (IOException e) {
				}
			}
			*/
		}
	}
	
	
	
	/**
	 * Establishes a new shared memory segment mapping using the given location and the given size.
	 * If the smseg does not exist, an exception will be thrown.
	 *  
	 * @param location - an identifier of the location of the smseg.
	 * @param size - the size of the smseg in bytes.
	 * @throws SMException
	 */
	public SharedMemory(String location, int size) throws SMException {
		this(location, size, false);
	}

	
	/**
	 * Establishes a new shared memory segment mapping using the given location and the given size.
	 * The create parameter states whether the shared memory segment should be created or not.
	 * If the smseg should be created, and is already found an exception is thrown.
	 * If the smseg should only be opened, and does not exist, an exception will be thrown.
	 *  
	 * @param location - an identifier of the location of the smseg.
	 * @param size - the size of the smseg in bytes.
	 * @param create - a flag that indicates whether the shared memory segment should be created or not.
	 * @throws SMException - In case we do not need to create the shared-segment and it does not exist.
	 */
	public SharedMemory(String location, int size, boolean create) throws SMException {
		this.location = location;
		this.size = size;
		
		// Create (and open) the RW-lock of the shared memory segment
		rwlockAddress = rwlockOpen(locationToRWlockName(location));
		rwlockReadsCounter = 0;
		rwlockWritesCounter = 0;
		rwlockOpenType = 0;
		
		// Try to open it
		smsegId = openSmSeg(location);
		
		// If it does not exist and we don't need to create it, throw exception
		if ((smsegId == -1) &&
			(!create))
			throw new SMException("Could not open the shared-segment: location does not exist");
		
		if (smsegId == -1) {
			// Create a new one (will be opened upon creation)
			smsegId = createSmSeg(location, size);
			if (smsegId == -1)
				throw new SMException("Could not create the shared memory segment");
		}
		
		// mmap to the smseg
		mmapAddress = mmap(smsegId, size);
		if (mmapAddress == -1)
			throw new SMException("Could not mmap to the shared memory segment");
	}

	/**
	 * Returns the RW-lock name that corresponds to the given location.
	 * 
	 * @param location - The location of the shared memory segment.
	 * @return The RW-lock name that corresponds to the given location.
	 */
	private static String locationToRWlockName(String location) {
		return location.replace('/', '_');
	}

	/**
	 * Close the current shared memory segment
	 */
	public void close() throws SMException {
		closeSmSeg(smsegId);
		rwlockClose(rwlockAddress);
	}
	
	/**
	 * Destroy and remove the current shared memory segment
	 */
	public void destroy() throws SMException {
		removeSmSeg(location);
		// Destroy the RW-lock of the shared memory segment
		rwlockDestroy(locationToRWlockName(location));
	}
	
	/**
	 * Write an array of bytes into a specified location in the segment, starting with a
	 * particular byte in the buffer and continuing for a specified number of bytes.
	 * 
	 * @param buf
	 *        The data to write.
	 * @param bufOffset
	 *        The location, relative to the start of the buffer, where the data should be
	 *        obtained.
	 * @param bufLength
	 *        The number of bytes to write.
	 * @param segmentOffset
	 *        The location in the buffer where the data should be written.
	 * @throws IPCException
	 *         The method will throw this exception if the segment is not actually
	 *         connected to the underlying shared memory block.
	 */
	public void write(byte[] buf, int bufOffset, int bufLength, int segmentOffset) throws SMException {
		if (bufLength + bufOffset > buf.length)
			throw new SMException("Given number of bytes to write is larger than the buffer itself");
		if (bufLength > 0)
			mmapWrite(mmapAddress, buf, bufOffset, bufLength, segmentOffset);
	}

	public void write(byte value, int offset) throws SMException {
		byte[] buf = new byte[1];
		buf[0] = value;
		write(buf);
	}

	public void write(byte[] buf) throws SMException {
		write(buf, 0, buf.length, 0);
	}

	public void write(int value, int offset) throws SMException {
		ByteBuffer buf = ByteBuffer.allocate(Integer.SIZE >> 3);
		buf.putInt(value);
		write(buf.array(), offset);
	}

	public void write(byte[] buf, int segmentOffset) throws SMException {
		write(buf, 0, buf.length, segmentOffset);
	}

	/**
	 * <P>
	 * Read bytes up to a specified number of bytes into the provided buffer.
	 * </P>
	 * <P>
	 * This method returns the number of bytes that were actually read. If fewer bytes are
	 * left in the segment than requested, then the method returns the number of bytes
	 * read.
	 * </P>
	 * 
	 * @param soffset
	 *        The offset within the segment where the read should start.
	 * @param buf
	 *        The buffer where the bytes read should be stored.
	 * @param boffset
	 *        The offset into buf where the bytes should be placed.
	 * @param length
	 *        The number of bytes to try to read.
	 * @return The number of bytes actually read. See description for details.
	 * @throws IPCException
	 *         This method throws this exception with the error set to
	 *         {@link Errors#NotConnected} if the instance has not been connected
	 *         to a segment yet.
	 */
	public int read(int soffset, byte[] buf, int boffset, int length) throws SMException {
		if (length + boffset > buf.length)
			throw new SMException("Given number of bytes to read is larger than the buffer itself");
		if (length > 0)
			return mmapRead(mmapAddress, buf, boffset, length, soffset);
		else
			return 0;
	}

	/**
	 * <P>
	 * Read bytes from the segment into a buffer, storing them in a location offset from
	 * the start of the buffer.
	 * </P>
	 * <P>
	 * This method is equivalent to calling:
	 * </P>
	 * <CODE>
	 * <PRE>
	 *     read(0, buf, bufferOffset, buf.length);
	 * </PRE>
	 * </CODE>
	 * 
	 * @param buf
	 *        The buffer where the bytes read should be placed.
	 * @param bufferOffset
	 *        The location in the buffer where the bytes read should start.
	 * @return The number of bytes actually read.
	 * @throws IPCException
	 *         {@link #read(int, byte[], int, int)}.
	 * @see #read(int, byte[], int, int)
	 */
	public int read(byte[] buf, int bufferOffset) throws SMException {
		return read(0, buf, bufferOffset, buf.length);
	}

	/**
	 * Read bytes from the start of the segment.
	 * <P>
	 * This method is equivalent to calling
	 * </P>
	 * <CODE>
	 * <PRE>
	 *     read(0, buf, 0, buf.length);
	 * </PRE>
	 * </CODE>
	 * <P>
	 * See that method for details.
	 * </P>
	 * 
	 * @param buf
	 *        The buffer where bytes read should be placed.
	 * @return The number of bytes read.
	 * @throws IPCException
	 *         see {@link #read(int, byte[], int, int)}.
	 * @see {@link #read(int, byte[], int, int)}
	 */
	public int read(byte[] buf) throws SMException {
		return read(0, buf, 0, buf.length);
	}

	/**
	 * Read in data from the segment, starting at a particular location in the segment.
	 * <P>
	 * This method is equivalent to calling
	 * </P>
	 * <CODE>
	 * <PRE>
	 *     read(segmentOffset, buf, 0, buf.length);
	 * </PRE>
	 * </CODE>
	 * <P>
	 * See that method for details.
	 * </P>
	 * 
	 * @param segmentOffset
	 *        The byte index into the segment where reading should start.
	 * @param buf
	 *        Where the bytes read should be placed.
	 * @return The number of bytes actually read.
	 * @throws IPCException
	 *         see {@link #read(int, byte[], int, int)}
	 * @see #read(int, byte[], int, int)
	 */
	public int read(int segmentOffset, byte[] buf) throws SMException {
		return read(segmentOffset, buf, 0, buf.length);
	}

	/**
	 * Return the byte at a particular location in the segment.
	 * 
	 * @param offset
	 *        The location of the byte in the segment.
	 * @return The value of the byte.
	 * @throws IndexOutOfBoundsException
	 *         If offset is negative or larger than the size of the segment minus 1.
	 */
	public byte getByte(int offset) throws SMException {
		byte[] buf = new byte[1];
		read(offset, buf);
		return buf[0];
	}

	/**
	 * Reserve the shared memory segment for read.
	 * 
	 * @throws SMException
	 */
	public void lockRead() throws SMException {
		// Acquire the lock, if we haven't already acquired it to write
		// (if acquired for read, just increment the number, no need to access it again)
		rwlockReadsCounter++;
		if ((rwlockReadsCounter == 1) && 
				(rwlockWritesCounter == 0)) {
			rwlockOpenType = 0;
			rwlockAcquireRead(rwlockAddress);
		}
	}

	/**
	 * Release the shared memory segment for read.
	 * 
	 * @throws SMException
	 */
	public void unlockRead() throws SMException {
		rwlockReadsCounter--;
		unlock();
	}

	/**
	 * Reserve the shared memory segment for write.
	 * 
	 * @throws SMException
	 */
	public void lockWrite() throws SMException {
		// Cannot acquire for write if already acquired for read
		if (rwlockReadsCounter > 0)
			throw new SMException("Cannot upgrade a read lock to a write lock");

		if (rwlockWritesCounter == 0) {
			rwlockOpenType = 1;
			rwlockAcquireWrite(rwlockAddress);
		}
		rwlockWritesCounter++;
	}

	/**
	 * Release the shared memory segment for write.
	 * 
	 * @throws SMException
	 */
	public void unlockWrite() throws SMException {
		rwlockWritesCounter--;
		unlock();
	}

	private void unlock() {
		if ((rwlockReadsCounter == 0) &&
				(rwlockWritesCounter == 0)) {
			if (rwlockOpenType == 0) { // Read
				rwlockReleaseRead(rwlockAddress);
			} else { // Write
				rwlockReleaseWrite(rwlockAddress);
			}
		}
	}
	
	/**
	 * Put some data at a location.
	 * <P>
	 * The entire contents of the buffer are written to a position in the segment defined
	 * by the offset parameter. If the amount of data exceeds the size of the segment, the
	 * method throws an exception.
	 * </P>
	 * 
	 * @param offset
	 *        The location in the segment, in bytes, where the data will be written.
	 * @param data
	 *        The data to write. The contents of the array will be written.
	 * @throws IPCException
	 */
	public void put(int offset, byte[] data) throws SMException {
		write(data, offset);
	}
	/**
	 * <P>
	 * Retrieve a line of text from the segment.
	 * </P>
	 * <P>
	 * This method goes to the specified location in the segment and tries to read in
	 * characters. It continues until it hits the end of the segment, or it encounters the
	 * end of the string. The end of a string is denoted by any of the following sequences
	 * of characters:
	 * </P>
	 * <UL>
	 * <LI>'\n'</LI>
	 * <LI>'\r'</LI>
	 * <LI>'\n\r'</LI>
	 * </UL>
	 * <P>
	 * The method uses the underlying character set as defined by
	 * {@link Charset#defaultCharset()}
	 * </P>
	 * <P>
	 * The supplied buffer is used to determine the maximum length of the string in that,
	 * if no line termination has been encountered before a number of bytes equal to the
	 * size of the buffer are read, then the characters accumulated so far are returned.
	 * </P>
	 * <P>
	 * Any line termination character(s) are not included in the string returned.
	 * </P>
	 * 
	 * @param offset
	 *        The location in the segment to start reading.
	 * @param buffer
	 *        A buffer to contain the bytes from the shared memory segment.
	 * @return The resulting string.
	 * @throws IOException
	 *         If the method tries to read past the end of the segment, either because it
	 *         started outside the segment or because the string did not have any line
	 *         termination.
	 * @throws IPCException
	 *         This exception is thrown if the object is not currently connected to a
	 *         shared memory segment.
	 */
	/*
	public String getLine(int offset, byte[] buffer) throws SMException {
		// TODO - complete
		return null;
	}
	*/

	/**
	 * Return a string from the specified location in the segment; the string may only
	 * take the specified number of bytes.
	 * <P>
	 * This method is equivalent to calling
	 * </P>
	 * <CODE>
	 * <PRE>
	 *     byte[] buf = new byte[maxBytes];
	 *     getLine(offset, buf);
	 * </PRE>
	 * </CODE>
	 * <P>
	 * See that method for details.
	 * </P>
	 * <P>
	 * If the underlying string does not end after maxBytes, the method returns the
	 * substring that takes up that number of bytes.
	 * </P>
	 * 
	 * @param offset
	 *        The location within the segment where the string starts.
	 * @param maxBytes
	 *        The maximum number of bytes for the string.
	 * @return The string found.
	 * @throws IOException
	 *         See {@link #getLine(int, byte[])}.
	 * @throws IPCException
	 *         See {@link #getLine(int, byte[])}.
	 * @see #getLine(int, byte[])
	 */
	/*
	public String getLine(int offset, int maxBytes) throws SMException {
		byte[] buffer = new byte[maxBytes];
		return getLine(offset, buffer);
	}
	*/

	
	public void putString(int offset, String s) throws SMException {
		write(s.getBytes(), offset);
	}

	/*
	public void putLine(int offset, String s) {
		// TODO - complete
		
		//s = s + '\n';
		//putString(offset, s);
		
	}
	*/
	
	
	
	
	/**
	 * Creates a new shared memory segment.
	 * The smseg will be identified by the given loaction, and will
	 * have the given size.
	 * This method should be called once (upon the first time a shared
	 * memory segment should be created).
	 * 
	 * @param location - an identifier of the location of the smseg.
	 * @param size - the size of the smseg in bytes.
	 * @return the identifier of the created smseg (the FD).
	 */
	private native int createSmSeg(String location, int size)
			throws SMException;


	/**
	 * Opens a shared memory segment.
	 * The smseg will be identified by the given loaction.
	 * This method should be called by any process that needs to access
	 * the shared memory segment, and haven't opened it using the createSmSeg
	 * method.
	 * 
	 * @param location - an identifier of the location of the smseg.
	 * @return a unique identifying number for the given smseg.
	 */
	private native int openSmSeg(String location)
			throws SMException;


	/**
	 * Closes a given shared memory segment.
	 * Should be called by any process which does not need the use of the memory
	 * segement anymore.
	 * 
	 * @param smsegId - the unique id of the shared memory segment
	 */
	private native void closeSmSeg(int smsegId)
			throws SMException;


	/**
	 * Destroys and removes the given Shared memory segment.
	 * This method should be called only once, to remove the shared memory
	 * segment.
	 * 
	 * @param location - an identifier of the location of the smseg.
	 */
	private native void removeSmSeg(String location)
			throws SMException;



	/**
	 * Opens an mmap to the given shared memory segment Id.
	 * The smseg should already be opened by each process.
	 * 
	 * @param smsegId - the unique id of the shared memory segment.
	 * @param size - the size of the smseg in bytes.
	 * @return the pointer to the beginning of the mmaped shared memory segment.
	 */
	private native long mmap(int smsegId, int size)
			throws SMException;



	/**
	 * Removes an mmap mapping.
	 * This method should be called by any process that called mmap.
	 * 
	 * @param mmapAddr - the pointer to the beginning of the mmaped shared memory segment.
	 * @param size - the size of the smseg in bytes.
	 * @return
	 */
	private native void munmap(long mmapAddr, int size)
			throws SMException;


	/**
	 * Reads length bytes from a the mmap (located at mmapAddr) from the given segmentOffset
	 * into the given buffer array.
	 *  
	 * @param mmapAddr - the pointer to the beginning of the mmaped shared memory segment.
	 * @param buf - the buffer array that will hold the returned data.
	 * @param bufOffset - the offset of the buffer where the data will be fetched into.
	 * @param length - the length of the data that needs to be fetched.
	 * @param segmentOffset - the offset to start reading from.
	 * @return the number of bytes actually read.
	 */
	private native int mmapRead(long mmapAddr, byte[] buf, int bufOffset, int length, int segmentOffset)
			throws SMException;


	/**
	 * Reads length bytes of the given buf to the mmap (located at mmapAddr) from 
	 * the given segmentOffset.
	 *  
	 * @param mmapAddr - the pointer to the beginning of the mmaped shared memory segment.
	 * @param buf - the buffer array that holds the data to write.
	 * @param bufOffset - the offset of the buffer to start writing to.
	 * @param length - the length of the data that needs to be written.
	 * @param segmentOffset - the offset to start writing to.
	 * @return the number of bytes actually read.
	 */
	private native void mmapWrite(long mmapAddr, byte[] buf, int bufOffset, int length, int segmentOffset)
			throws SMException;

	
	
	/* Methods for Read-Write-Lock */
	
	/**
	 * Opens and (optionally) creates a new RW-lock given its name.
	 * 
	 * @param name the name of the RW-lock
	 * @return The address of the RW-lock
	 * @throws SMException
	 */
	private native long rwlockOpen(String name);

	
	/**
	 * Closes the given RW-lock
	 * @param rwlockAddr The address of the RW-lock
	 * @throws SMException
	 */
	private native void rwlockClose(long rwlockAddr);


	/**
	 * Destroys the given RW-lock
	 * @param name the name of the RW-lock
	 * @throws SMException
	 */
	private native void rwlockDestroy(String name);

	
	/**
	 * Acquires a read-lock from the given RW-lock
	 * @param rwlockAddr The address of the RW-lock
	 * @throws SMException
	 */
	private native void rwlockAcquireRead(long rwlockAddr);

	
	/**
	 * Releases a read-lock from the given RW-lock
	 * @param rwlockAddr The address of the RW-lock
	 * @throws SMException
	 */
	private native void rwlockReleaseRead(long rwlockAddr);


	/**
	 * Acquires a write-lock from the given RW-lock
	 * @param rwlockAddr The address of the RW-lock
	 * @throws SMException
	 */
	private native void rwlockAcquireWrite(long rwlockAddr);


	/**
	 * Releases a write-lock from the given RW-lock
	 * @param rwlockAddr The address of the RW-lock
	 * @throws SMException
	 */
	private native void rwlockReleaseWrite(long rwlockAddr);

	
}
